import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.TreeMap;

/**
 * Authors - Oren Bracha, Rony Lahav and Yehuda Eterman
 * 
 * the class handles a single session from the side of the data sender
 *
 */
public class UDPSenderConnection implements Runnable {
	private DatagramSocket concreteSocket; // Will hold a reference for the specific socket
	private DatagramPacket firstPacket;
	private String receiverName;    // name of the receiver peer on the other side
	private String receiverIP;      // name of the receiver IP on the other side
	private int receiverPort;      // name of the receiver IP on the other side
	long uniqueID;
	
	private static String connType = "udp";
	
	DBCopy dbCopy = null;
	
	public UDPSenderConnection(String receiverName, String receiverIP, DatagramSocket ds, int reciverPort, DatagramPacket firstPacket, long uniqueID)
	{
		try {
			dbCopy = DataBase.getInstance().createCopy();
			concreteSocket = ds;
			this.firstPacket = firstPacket;
			concreteSocket.setSoTimeout(proj350.TIMEOUT_IDLE_PERIOD);
			this.receiverName = receiverName;
			this.receiverIP = receiverIP;
			this.receiverPort = reciverPort;
			this.uniqueID = uniqueID;
		}
		catch (SocketException e) {
		}
		
	}
	
	/**
	 * This method is the main entrance to this thread. It represents the sender side in a 
	 * tcp session between peers (the one who sends the update, entries, picture).
	 * It sends the update message and then wait for progress.  
	 */
	
	public void run()
	{
		try{
			updateSession();
		}
		catch (Exception e){
		}
		finally{
			proj350.removeThreadFromLivingList(uniqueID);
			concreteSocket.close();
		}
	}
	

	private void sendMessage(String message) throws IOException{
		DatagramPacket dp = new DatagramPacket(message.getBytes(),message.length(),InetAddress.getByName(receiverIP),receiverPort);
		concreteSocket.send(dp);
	}
	
	/**
	 * Receiving a single message
	 * @return a UDP packet containing the data
	 */
	private DatagramPacket readMessage() {
		try{
			byte[] buffer = new byte[proj350.getMSS()*2];
			DatagramPacket dp = new DatagramPacket(buffer,buffer.length);
			concreteSocket.receive(dp);
			return dp;
		}
		catch (IOException e){
			return null;
		}
		catch (Exception e){
			return null;
		}
	}

	/**
	 * 
	 * @return string from the bytes of the packet data 
	 */
	private String getPacketAsString(DatagramPacket dp){
		String msg = new String(dp.getData(),0,dp.getLength());
		return msg;
	}
	
	/**
	 * This method is the actual method for the session
	 * @throws IOException
	 */
	private void updateSession() {//throws IOException{
		try {
			String messageRead = new String(firstPacket.getData(),0,firstPacket.getLength());
			DatagramPacket dp = null;
			
			while (messageRead != null)
			{
				// Parse the message
				HashMap<String, String> parsedMessage = MessageCenter.getInstance().parseValidMessage(messageRead, receiverName, receiverIP, Integer.toString(concreteSocket.getPort()), proj350.getMyPeerName(), proj350.getMyIp(), Integer.toString(concreteSocket.getLocalPort()),false);
				
				// Failed to parse the message
				if (parsedMessage != null){
					
					//The message type is extracted
					String messageType = parsedMessage.get(MessageCenter.TYPE_MESSAGE);
				
					// Message type read is requestentry, take care of it
					if (messageType.compareTo(MessageCenter.TYPE_REQUESTENTRY) == 0) {
						Logger.getInstance().printInOutMsg(true, messageRead, receiverName, receiverIP, Integer.toString(firstPacket.getPort()), proj350.getMyPeerName(), proj350.getMyIp(), Integer.toString(concreteSocket.getLocalPort()), connType);
						processRequestEntryMessage(parsedMessage);
					}
					// Message type read is requestpicture, take care of it
					else if (messageType.compareTo(MessageCenter.TYPE_REQUESTPICTURE) == 0)
					{
						Logger.getInstance().printInOutMsg(true, messageRead, receiverName, receiverIP, Integer.toString(firstPacket.getPort()), proj350.getMyPeerName(), proj350.getMyIp(), Integer.toString(concreteSocket.getLocalPort()), connType);
						processRequestPictureMessage(parsedMessage);
					}
					else {
						Logger.getInstance().printErrorWrongFormat("unknown", messageRead, receiverName, receiverIP, Integer.toString(concreteSocket.getPort()), proj350.getMyPeerName(), proj350.getMyIp(), Integer.toString(concreteSocket.getLocalPort()),connType);
					}
				}
				else {
				}

				//read next message
				dp = readMessage();
				if (dp != null){
					messageRead = getPacketAsString(dp);
				}
				else
				{
					dp = readMessage();
					if (dp != null){
						messageRead = getPacketAsString(dp);
					}
					else
					{
						messageRead = null;
					}
				}
				
			}
			 // If I fail to read a message form the socket then I'm leaving the loop
		}
		catch (SocketTimeoutException e2)
		{
			// Time is up, no messages from reciepent
		}
		catch (Exception e){
		}
	}
	
	/**
	 * This method is responsible to deal with a RequestEntry message
	 * @throws IOException 
	 * 
	 */
	private void processRequestEntryMessage(HashMap<String, String> parsedMessage) throws IOException {
		// Determine if the message contains "list" or "range"
		String varType = parsedMessage.get(MessageCenter.TYPE_VAR);
		
		// Get the DB-version from the message
		String reqDBVersion = parsedMessage.get(MessageCenter.VAR_DBVERSION);
		
		// Neither "list" nor "range" in the message
		if (varType == null) {
			// Get all tuples from the db with DBVersion newer than the one in the message
			TreeMap<String, DBTuple> tuples = (TreeMap<String, DBTuple>)dbCopy.getTuplesByDbVersion(reqDBVersion);
			
			// Send all of the entry messages
			sendEntriesInList(tuples);
		}
		// The message contains "list" param
		else if (varType.compareTo(MessageCenter.VAR_LIST) == 0) {
			// Get the list of peers from the message
			String[] list = parsedMessage.get(MessageCenter.VAR_LIST).split(",");
			
			// Find each tuple requested and send it 
			for (int i=1; i<=list.length; ++i){
				DBTuple tuple = dbCopy.findTuple(list[i]);
				if (tuple != null){
					//create request entry message
					String entryMessage = MessageCenter.getInstance().createMessage_Entry(Integer.toString(i), Integer.toString(list.length), tuple.getDbVersion(), tuple.getPeerVersion(), tuple.getPeerName(), tuple.getIp(), tuple.getTcpPort(), tuple.getUdpPort(), tuple.getPhoneNum(), tuple.getEmailAddress(), tuple.getLastConnectionTime(), tuple.getStatusAsString(), tuple.getPicSize(), tuple.getPicVersion());	
					sendMessage(entryMessage);
					Logger.getInstance().printInOutMsg(false, entryMessage, proj350.getMyPeerName(), proj350.getMyIp(), Integer.toString(concreteSocket.getLocalPort()), receiverName, receiverIP, Integer.toString(concreteSocket.getPort()),connType);
				}
			}
		}
		// The message contains "range" param
		else if (varType.compareTo(MessageCenter.VAR_RANGE) == 0) {
			
			// Get the range of peers from the DB 
			TreeMap<String, DBTuple> tuples = (TreeMap<String, DBTuple>)DataBase.getInstance().getRangeOfPeers(parsedMessage.get(MessageCenter.VAR_RANGEFIRST), parsedMessage.get(MessageCenter.VAR_RANGELAST),parsedMessage.get(MessageCenter.VAR_DBVERSION));
			
			// Send the entries
			sendEntriesInList(tuples);
		}
	}
	
	/**
	 * This method is responsible to deal with a RequestPicture message
	 * @throws IOException 
	 * 
	 */
	private void processRequestPictureMessage(HashMap<String, String> parsedMessage) throws IOException {
		// Determine if the message contains "list" or "range"
		String varType = parsedMessage.get(MessageCenter.TYPE_VAR);
		
		//check if specific peer or all peers
		String peerName = parsedMessage.get(MessageCenter.VAR_PEERNAME);
		
		// Get the DB-version from the message
		String reqDBVersion = parsedMessage.get(MessageCenter.VAR_DBVERSION);
		
		//get receive window from the message
		int rcvWin = Integer.parseInt(parsedMessage.get(MessageCenter.VAR_RCVWIN));
		
		//type without peername
		if (peerName == null){
			// Get all tuples from the db with DBVersion newer than the one in the message
			TreeMap<String, DBTuple> peersAndTuples = (TreeMap<String, DBTuple>)dbCopy.getTuplesByDbVersion(reqDBVersion);
			Collection<DBTuple> tuples = peersAndTuples.values();
			for (DBTuple tuple : tuples) {
				LinkedList<String> pictureMessages = getPictureMessages(tuple, rcvWin);
				if (pictureMessages != null){
					try {
						sendPicturesInList(pictureMessages);
					} catch (Exception e) {
					}
				}
			}
		}
		// Neither "list" nor "range" in the message
		else if (varType == null) {
			// Get the tuple from the db
			DBTuple tuple = dbCopy.findTuple(peerName);
			//if the peer i have is more updated than the dbversion i got
			if (DateStringConverter.compare2Dates(tuple.getPeerCommitTime(), reqDBVersion)>0){
				LinkedList<String> pictureMessages = getPictureMessages(tuple, rcvWin);
				if (pictureMessages != null){
					try {
						sendPicturesInList(pictureMessages);
					} catch (Exception e) {
					}
				}
			}
		}
		// The message contains "list" param
		else if (varType.compareTo(MessageCenter.VAR_LIST) == 0) {
			// Get the tuple from the db
			DBTuple tuple = dbCopy.findTuple(peerName);
			//if the peer i have is more updated than the dbversion i got
			String segmentsList = parsedMessage.get(MessageCenter.VAR_LIST);
			LinkedList<String> pictureMessages = getPictureMessages(tuple, rcvWin, segmentsList);
			if (pictureMessages != null){
				try {
					//send segments of picture that were requested
					sendPicturesInList(pictureMessages);
				} catch (Exception e) {
				}
				}
		}
		// The message contains "range" param
		else if (varType.compareTo(MessageCenter.VAR_RANGE) == 0) {
			// Get the tuple from the db
			DBTuple tuple = dbCopy.findTuple(peerName);
			//find the first and last segments of the range from the request message
			String first = parsedMessage.get(MessageCenter.VAR_RANGEFIRST);
			String last = parsedMessage.get(MessageCenter.VAR_RANGELAST);
			LinkedList<String> pictureMessages = getPictureMessages(tuple, rcvWin, "", first, last);
			if (pictureMessages != null){
				try {
					//send segments of picture that were requested
					sendPicturesInList(pictureMessages);
				} catch (Exception e) {
				}
			}
		}
	}
	
	/**
	 * 
	 * This method gets a list of tuples (entries) and sends them to the receiver
	 */
	public void sendEntriesInList(TreeMap<String, DBTuple> tuples) throws IOException{
		int numOfTuples = tuples.size();
		
		Collection<DBTuple> c = tuples.values();
		int tupleIndex = 1;
		for (DBTuple tuple : c) {
			//create entry message of the relevant peer
			String entryMessage = MessageCenter.getInstance().createMessage_Entry(Integer.toString(tupleIndex), Integer.toString(numOfTuples), tuple.getDbVersion(), tuple.getPeerVersion(), tuple.getPeerName(), tuple.getIp(), tuple.getTcpPort(), tuple.getUdpPort(), tuple.getPhoneNum(), tuple.getEmailAddress(), tuple.getLastConnectionTime(), tuple.getStatusAsString(), tuple.getPicSize(), tuple.getPicVersion());
			//send requested entry
			sendMessage(entryMessage);
			Logger.getInstance().printInOutMsg(false, entryMessage, proj350.getMyPeerName(), proj350.getMyIp(), Integer.toString(concreteSocket.getLocalPort()), receiverName, receiverIP, Integer.toString(receiverPort),connType);
			++tupleIndex;
		}
	}
	
	//calculates how many fragments to send and returns a list of picture messages accordingly
	private LinkedList<String> getPictureMessages(DBTuple tuple, int rcvWin) throws IOException{
		int picSize = Integer.parseInt(tuple.getPicSize())*2;
		int numOfFragments = picSize/proj350.getMSS() + (picSize%proj350.getMSS()==0?0:1);
		return getPictureMessages(tuple, rcvWin, "", "1", Integer.toString(numOfFragments));
	}
	
	/**
	 * 
	 * @param tuple
	 * @param rcvWin - the receive window determines the amount of fragments that can get in the message
	 * @param listOfSegments - segments to get from the picture
	 * @return list of picture segments
	 */
	private LinkedList<String> getPictureMessages(DBTuple tuple, int rcvWin, String listOfSegments){
		LinkedList<String> pictureMessages = new LinkedList<String>();
		String[] segments = listOfSegments.split(",");
		int[] segmentsNumbers = null;
		String pictureMessage = null;

		byte[][] tempData = null;
		byte[][] data = null;
		int picLength = Integer.parseInt(tuple.getPicSize());
		
		//find the fragment numbers from the list
		segmentsNumbers = new int[segments.length];
		int numOfSegments = segmentsNumbers.length;
		for (int i = 0; i < segments.length; i++) {
			segmentsNumbers[i]=Integer.parseInt(segments[i]);
		}
		int sizeOfMessage = 0;
		int numOfFragmentsToSend = 0;
		
		//get specific list fragments of picture encoded in hexadecimal format
		try {
			tempData = PictureHandler.getPartsofPicInHexa(segmentsNumbers, PictureHandler.getPictureForTransfer(tuple.getPeerName()), picLength, proj350.getMSS());
		} catch (IOException e) {
		}
		if (tempData != null){
			//checking if we do not go over the rcvWin
			while(sizeOfMessage <= rcvWin && numOfFragmentsToSend < numOfSegments){
				sizeOfMessage += tempData[numOfFragmentsToSend].length;
				numOfFragmentsToSend++;
			}
			
			//if we went over the receive window then send one less fragment
			if(sizeOfMessage > rcvWin){
				numOfFragmentsToSend--;
			}
			
			//copying the fragments that we deem worthy 
			data = new byte[numOfFragmentsToSend][];
			for(int i =0 ;i< numOfFragmentsToSend; ++i){
				data[i] = tempData[i];
			}		
			
			String picversion = tuple.getPicVersion();
			String peername = tuple.getPeerName();
			
			if (data == null)
				return null;
			
			//add fragments to send to the PICTUE message
			for(int i=1;i<=numOfFragmentsToSend;++i){
				pictureMessage = MessageCenter.getInstance().createMessage_Picture(Integer.toString(i), Integer.toString(numOfFragmentsToSend), picversion, peername, new String(data[i-1]));
				pictureMessages.add(pictureMessage);
			}
			return pictureMessages;
		}
		else
			return null;
	}
	
	/**
	 * 
	 * @param tuple
	 * @param rcvWin - the receive window determines the amount of fragments that can get in the message
	 * @param first - index of first fragment to send
	 * @param last - index of last fragment to send
	 * @return - a list of the requested picture fragments
	 * @throws IOException
	 */
	private LinkedList<String> getPictureMessages(DBTuple tuple, int rcvWin, String list, String first, String last) throws IOException{
		LinkedList<String> pictureMessages = new LinkedList<String>();
		//find number of fragments
		int lastIndex = Integer.parseInt(last);
		int firstIndex = Integer.parseInt(first);
		int numOfSegments = lastIndex-firstIndex+1;
		String pictureMessage = null;
		byte[][] tempData = null;
		byte[][] data = null;
		int picLength = Integer.parseInt(tuple.getPicSize());
		
		int sizeOfMessage = 0;
		int numOfFragmentsToSend = 0;
		//get specific range of fragments of picture encoded in hexadecimal format
		tempData = PictureHandler.getPartsofPicInHexa(firstIndex, lastIndex, PictureHandler.getPictureForTransfer(tuple.getPeerName()), picLength, proj350.getMSS());
		if (tempData != null){
			//checking if we do not go over the rcvWin
			while(sizeOfMessage <= rcvWin && numOfFragmentsToSend < numOfSegments){
				sizeOfMessage += tempData[numOfFragmentsToSend].length;
				numOfFragmentsToSend++;
			}
			
			//if we went over the receive window then send one less fragment
			if(sizeOfMessage > rcvWin){
				numOfFragmentsToSend--;
			}
			
			data = new byte[numOfFragmentsToSend][];
			
			//copying the fragments that we deem worthy
			for(int i =0 ;i< numOfFragmentsToSend; ++i){
				data[i] = tempData[i];
			}		
			
			String picversion = tuple.getPicVersion();
			String peername = tuple.getPeerName();
	
			if (data == null)
				return null;
			//add fragments to send to the PICTUE message
			for(int i=1;i<=numOfFragmentsToSend;++i){
				pictureMessage = MessageCenter.getInstance().createMessage_Picture(Integer.toString(i), Integer.toString(numOfFragmentsToSend), picversion, peername, new String(data[i-1]));
				pictureMessages.add(pictureMessage);
			}
			return pictureMessages;
		}
		else
			return null;
	}
	
	/**
	 * sending the list of fragments one at a time
	 * @param list - list of fragment
	 * @throws IOException 
	 */
	private void sendPicturesInList(LinkedList<String> list) throws IOException{
		for (String msg : list) {
			sendMessage(msg);
			Logger.getInstance().printInOutMsg(false, msg, proj350.getMyPeerName(), proj350.getMyIp(), Integer.toString(concreteSocket.getLocalPort()), receiverName, receiverIP, Integer.toString(receiverPort),connType);
		}
	}
	
	public long getUniqueID(){
		return uniqueID;
	}
}
